package com.gitee.qdbp.base.orm.mybatis.interceptor;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.binding.MapperMethod.MethodSignature;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.session.defaults.DefaultSqlSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.ReflectionUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.gitee.qdbp.able.exception.ServiceException;
import com.gitee.qdbp.able.jdbc.paging.Paging;
import com.gitee.qdbp.base.annotation.OperateTraces;
import com.gitee.qdbp.base.controlling.ControllingTools;
import com.gitee.qdbp.base.controlling.IControllingVars;
import com.gitee.qdbp.base.controlling.OperateTracesBean;
import com.gitee.qdbp.base.enums.OperateType;
import com.gitee.qdbp.tools.utils.StringTools;
import com.gitee.qdbp.tools.utils.VerifyTools;

/**
 * 操作轨迹记录, 查找带有@OperateTraces的参数对象, 提取操作日志信息
 *
 * @author zhaohuihua
 * @version 170615
 */
@Intercepts({
        @Signature(type = Executor.class, method = "query",
            args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }),
        @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }) })
public class OperateTracesSimpleInterceptor extends BaseExecutorInterceptor {

    private static Logger log = LoggerFactory.getLogger(OperateTracesSimpleInterceptor.class);

    private IControllingVars vars;

    public OperateTracesSimpleInterceptor(IControllingVars vars) {
        this.vars = vars;
    }

    /**
     * 记录日志的数据库操作次数上限, 超过此上限的不再记录 <br>
     * 因为有些导入操作可能会有几千次的数据库操作 <br>
     * 
     * @return 记录日志的数据库操作次数上限, 0=无上限
     */
    protected int getDbInvokeMaxTimes() {
        return 50;
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Date time = new Date();

        Object returns = null;
        try {
            returns = invocation.proceed();
            return returns;
        } catch (Throwable e) {
            returns = e;
            throw e;
        } finally {
            try {
                catchOperateTraces(invocation, returns, time);
            } catch (Throwable e) {
                log.error("Catch operate traces error.", e);
            }
        }
    }

    private void catchOperateTraces(Invocation invocation, Object returns, Date startTime) throws ServiceException {
        vars.addDbInvokeTimes();

        Object[] args = invocation.getArgs();

        MappedStatement statement = (MappedStatement) args[0];
        String id = statement.getId();
        int lastdot = id.lastIndexOf('.');
        String dao = id.substring(0, lastdot);
        String method = id.substring(lastdot + 1);
        SqlCommandType cmd = statement.getSqlCommandType();
        OperateType operateType = ControllingTools.convertOperateType(cmd);

        Class<?> daoClass;
        try {
            daoClass = Class.forName(dao);
        } catch (ClassNotFoundException e) {
            return;
        }
        OperateTraces daoAnnotation = findDaoMethodAnnotation(daoClass, method);
        if (daoAnnotation == null || !daoAnnotation.enable()) {
            return;
        }

        Object params = getMethodArgs(args[1]);

        int max = getDbInvokeMaxTimes(); // 0=无限制
        if (max <= 0 || vars.getDbInvokeTimes(false) <= max) {
            Date now = new Date();
            long executeTime = now.getTime() - startTime.getTime();
            String executeMethod = getSimpleMethodSignature(daoClass, method, params);
            OperateTracesBean trace = new OperateTracesBean();
            fillOperateTracesDetails(trace, params);
            trace.setDataValue(StringTools.ellipsis(JSON.toJSONString(params), 500));
            trace.setOperateType(operateType);
            trace.setCreateTime(startTime);
            trace.setExecuteTime((int) executeTime);
            trace.setExecuteMethod(executeMethod);
            if (VerifyTools.isNotBlank(daoAnnotation.operate())) {
                trace.setExecuteDesc(daoAnnotation.operate());
            }
            if (VerifyTools.isBlank(trace.getDataType())) {
                // 最开始设计sys_operate_traces表的data_type是必填的
                trace.setDataType("{unknown}");
            }
            if (returns instanceof Throwable) {
                trace.setAffectedRows(-1); // -1表示报错
            } else if ("update".equals(invocation.getMethod().getName())) {
                // 更新操作受影响行数取Executor.update()的返回值
                if (returns instanceof Number) {
                    trace.setAffectedRows(((Number) returns).intValue());
                }
            } else if ("query".equals(invocation.getMethod().getName())) {
                // 查询操作受影响行数取Executor.query()的返回数量
                if (returns instanceof Collection) { // 查询返回列表
                    trace.setAffectedRows(((Collection<?>) returns).size());
                } else if (returns != null) { // 查询返回对象
                    trace.setAffectedRows(1);
                }
            }
            vars.addTraces(trace);
        }
    }

    protected String getSimpleMethodSignature(Class<?> clazz, String method, Object params) {
        String ellipsis = params == null ? "" : ".";
        if (params instanceof Collection) {
            int size = ((Collection<?>) params).size();
            if (size > 10) {
                ellipsis = String.valueOf(size);
            } else {
                ellipsis = StringTools.pad("", '.', size);
            }
        }
        return clazz.getSimpleName() + "." + method + "(" + ellipsis + ")";
    }

    protected OperateTraces findDaoMethodAnnotation(Class<?> clazz, String methodName) {
        Method method = null;
        Class<?> c = clazz;
        while (method == null && c != null) {
            Method[] methods = clazz.getDeclaredMethods();
            for (Method m : methods) {
                if (m.getName().equals(methodName)) {
                    method = m; // DAO类不允许有重名方法, 因此不需要判断参数
                    break;
                }
            }
            c = c.getSuperclass();
        }
        if (method == null) {
            return null;
        }

        OperateTraces annotation = AnnotationUtils.findAnnotation(method, OperateTraces.class);
        if (annotation == null) {
            annotation = AnnotationUtils.findAnnotation(clazz, OperateTraces.class);
        }
        return annotation;
    }

    /**
     * 还原方法参数, 将Executor的参数尽量还原为DAO的参数
     * 
     * @param object Executor参数
     * @return DAO参数
     * @see MethodSignature#convertArgsToSqlCommandParam(Object[])
     * @see DefaultSqlSession#wrapCollection(Object)
     */
    protected Object getMethodArgs(Object object) {
        if (object instanceof MapperMethod.ParamMap<?>) {
            List<Object> list = new ArrayList<>();
            MapperMethod.ParamMap<?> map = (MapperMethod.ParamMap<?>) object;
            for (int i = 0; i < map.size(); i++) {
                String key = "param" + String.valueOf(i + 1);
                if (map.containsKey(key)) {
                    list.add(map.get(key));
                }
            }
            return list;
        } else if (object instanceof DefaultSqlSession.StrictMap<?>) {
            List<Object> list = new ArrayList<>();
            DefaultSqlSession.StrictMap<?> map = (DefaultSqlSession.StrictMap<?>) object;
            for (Object o : map.values()) {
                if (o instanceof Collection) {
                    for (Object s : (Collection<?>) o) {
                        list.add(s);
                    }
                } else if (o.getClass().isArray()) {
                    for (Object s : (Object[]) o) {
                        list.add(s);
                    }
                } else if (o != null) {
                    list.add(o);
                }
            }
            if (list.size() == 1) {
                return list.get(0);
            } else {
                return list;
            }
        } else {
            return object;
        }
    }

    protected void fillOperateTracesDetails(OperateTracesBean trace, Object o) {
        if (o == null || o instanceof Paging || o instanceof RowBounds) {
            return;
        }
        if (o instanceof Collection) {
            Collection<?> list = (Collection<?>) o;
            for (Object item : list) {
                String dataType = ControllingTools.findTableName(item.getClass());
                trace.setDataType(dataType);
                break;
            }
            return;
        }
        if (o.getClass().isArray()) {
            Object[] array = (Object[]) o;
            for (Object item : array) {
                String dataType = ControllingTools.findTableName(item.getClass());
                trace.setDataType(dataType);
                break;
            }
            return;
        }

        Object target = o;
        OperateTraces annotation;
        do {
            Class<?> clazz = target.getClass();
            annotation = AnnotationUtils.findAnnotation(clazz, OperateTraces.class);
            if (annotation == null || !annotation.enable()) {
                return;
            } else if (VerifyTools.isBlank(annotation.target())) {
                break;
            }
            String name = annotation.target();
            Field field = ReflectionUtils.findField(clazz, name);
            ReflectionUtils.makeAccessible(field);
            target = ReflectionUtils.getField(field, target);
        } while (target != null);

        if (target == null || annotation == null || !annotation.enable()) {
            return;
        } else {
            fillOperateTracesDetails(trace, target, annotation);
        }
    }

    protected void fillOperateTracesDetails(OperateTracesBean trace, Object o, OperateTraces annotation) {
        if (!annotation.enable()) {
            return;
        }

        Class<?> clazz = o.getClass();

        String dataType = ControllingTools.findTableName(clazz);
        trace.setDataType(dataType);

        JSONObject json = (JSONObject) JSON.toJSON(o);

        String className = clazz.getName();
        String idField = annotation.id();
        if (VerifyTools.isBlank(idField)) {
            throw new IllegalStateException("@OperateTraces(id) is required. " + className);
        } else if (ReflectionUtils.findField(clazz, idField) == null) {
            throw new IllegalStateException("@OperateTraces(id) " + idField + " not found in " + className);
        } else {
            String dataId = json.getString(idField);
            trace.setDataId(dataId);
        }
        String codeField = annotation.code();
        if (VerifyTools.isBlank(codeField)) {
            // 非必填
        } else if (ReflectionUtils.findField(clazz, codeField) == null) {
            throw new IllegalStateException("@OperateTraces(code) " + codeField + " not found in " + className);
        } else {
            String dataCode = json.getString(codeField);
            trace.setDataCode(dataCode);
        }
        String descField = annotation.desc();
        if (VerifyTools.isBlank(descField)) {
            // 非必填
        } else if (ReflectionUtils.findField(clazz, descField) == null) {
            throw new IllegalStateException("@OperateTraces(desc) " + descField + " not found in " + className);
        } else {
            String dataDesc = json.getString(descField);
            trace.setDataDesc(dataDesc);
        }
    }

}
